package com.dotspots.rpcplus.codegen.jscollections;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.lang.reflect.Array;

import com.dotspots.rpcplus.client.jscollections.RpcUtils;
import com.google.gwt.core.client.JavaScriptObject;

/**
 * Generates a set of strongly-typed and generic GWT collections for use with Thrift.
 * 
 * It automatically handles translation of GWT longs to double pairs and back for hosted mode.
 */
public class CollectionGen {
	private static final Class<?>[] PRIMITIVE_CLASSES = new Class<?>[] { int.class, boolean.class, double.class, long.class, String.class };
	private final File outputDirectory;
	private final String packageName;

	public enum Type {
		LIST, SET, MAP
	}

	public CollectionGen(File outputDirectory, String packageName) {
		this.outputDirectory = outputDirectory;
		this.packageName = packageName;
	}

	public void generateCode() throws FileNotFoundException {
		writeList(Object.class);
		for (Class<?> clazz : PRIMITIVE_CLASSES) {
			writeList(clazz);
		}

		writeSet(int.class);
		writeSet(String.class);

		writeMap(int.class, Object.class);
		for (Class<?> clazz : PRIMITIVE_CLASSES) {
			writeMap(int.class, clazz);
		}

		writeMap(String.class, Object.class);
		for (Class<?> clazz : PRIMITIVE_CLASSES) {
			writeMap(String.class, clazz);
		}

		for (Class<?> clazz : PRIMITIVE_CLASSES) {
			writeProcedure(clazz);
			writeProcedure(String.class, clazz);
			writeProcedure(int.class, clazz);
		}

		writeProcedure(Object.class);
		writeProcedure(String.class, Object.class);
		writeProcedure(int.class, Object.class);
	}

	private String getName(Class<?> clazz) {
		if (clazz == int.class) {
			return "Int";
		}

		if (clazz == boolean.class) {
			return "Bool";
		}

		if (clazz == double.class) {
			return "Double";
		}

		if (clazz == long.class) {
			return "Long";
		}

		if (clazz == String.class) {
			return "String";
		}

		return "";
	}

	private String getNameWithObject(Class<?> clazz) {
		if (clazz == Object.class) {
			return "Object";
		}

		return getName(clazz);
	}

	private String getSafeSetter(Class<?> clazz) {
		if (clazz == long.class) {
			return "@" + RpcUtils.class.getName() + "::toDoubles(J)(value)";
		}

		return "value";
	}

	private String getSafeGetter(Class<?> clazz) {
		if (clazz == int.class || clazz == double.class) {
			return "%s || 0";
		}

		if (clazz == boolean.class) {
			return "!!%s";
		}

		if (clazz == long.class) {
			return "@" + RpcUtils.class.getName() + "::fromDoubles(L" + JavaScriptObject.class.getName().replace('.', '/')
					+ ";)(%s || [0,0])";
		}

		return "%s || null";
	}

	private void writeList(Class<?> elem) throws FileNotFoundException {
		String name = "List";
		name += getName(elem);

		writeCollection(name, Type.LIST, int.class, elem);
	}

	private void writeSet(Class<?> elem) throws FileNotFoundException {
		String name = "Set";
		name += getName(elem);

		writeCollection(name, Type.SET, elem, null);
	}

	private void writeMap(Class<?> key, Class<?> value) throws FileNotFoundException {
		String name = "Map";
		name += getName(key);
		name += getName(value);

		writeCollection(name, Type.MAP, key, value);
	}

	private String getProcedureName(Class<?> class1, boolean includeGenerics) {
		return getProcedureName(class1, null, includeGenerics);
	}

	private String getProcedureName(Class<?> class1, Class<?> class2, boolean includeGenerics) {
		String name = "JsRpc" + getNameWithObject(class1) + getNameWithObject(class2) + "Procedure";
		return includeGenerics && (class1 == Object.class || class2 == Object.class) ? name + "<E>" : name;
	}

	private void writeProcedure(Class<?> class1) throws FileNotFoundException {
		writeProcedure(class1, null);
	}

	private void writeProcedure(Class<?> class1, Class<?> class2) throws FileNotFoundException {
		File file = new File(outputDirectory, getProcedureName(class1, class2, false) + ".java");

		String keyType = (class1 == Object.class) ? "E" : class1.getSimpleName();
		String valueType = (class2 == null || class2 == Object.class) ? "E" : class2.getSimpleName();

		System.out.println(file.getName());
		PrintWriter printWriter = new PrintWriter(file);

		printWriter.println("// AUTOGENERATED: See " + getClass().getName() + " for more details");
		printWriter.println("package " + packageName + ";");
		printWriter.println();

		printWriter.println("public interface " + getProcedureName(class1, class2, true) + " {");

		printWriter.println("    /**");
		printWriter.println("     * Executes this procedure. A false return value indicates that");
		printWriter.println("     * the application executing this procedure should not invoke this");
		printWriter.println("     * procedure again.");
		printWriter.println("     */");
		if (class2 == null) {
			printWriter.println("    public boolean execute(" + keyType + " value);");
		} else {
			printWriter.println("    public boolean execute(" + keyType + " a, " + valueType + " b);");
		}
		printWriter.println("}");
		printWriter.close();
	}

	private String getBinaryName(Class<?> clazz) {
		if (clazz == null) {
			return null;
		}

		final String arrayName = Array.newInstance(clazz, 0).getClass().getName();
		return arrayName.substring(1).replace('.', '/');
	}

	private void writeCollection(String name, Type type, Class<?> key, Class<?> value) throws FileNotFoundException {
		final String fullName = "JsRpc" + name;
		File file = new File(outputDirectory, fullName + ".java");

		System.out.println(fullName);
		PrintWriter printWriter = new PrintWriter(file);

		boolean getter = (value != null);
		String genericsFull = (value == Object.class) ? "<E> " : "";
		String genericsShort = (value == Object.class) ? "<E>" : "";
		String valueType = (value == null || value == Object.class) ? "E" : value.getSimpleName();
		String keyType = (key == null || key == Object.class) ? "E" : key.getSimpleName();
		String valueBinaryName = getBinaryName(value);
		String keyBinaryName = getBinaryName(key);
		String indexer = (key == String.class) ? "['_' + idx]" : "[idx]";
		String keyString = (key == String.class) ? "'_' + idx" : "idx";
		String safeGetter = getSafeGetter(value);
		String safeSetter = getSafeSetter(value);

		try {
			printWriter.println("// AUTOGENERATED: See " + getClass().getName() + " for more details");
			printWriter.println("package " + packageName + ";");
			printWriter.println();

			printWriter.println("import com.google.gwt.core.client.JavaScriptObject;");
			printWriter.println("import com.google.gwt.core.client.UnsafeNativeLong;");
			printWriter.println("import com.google.gwt.core.client.GWT;");
			printWriter.println("import com.google.gwt.lang.LongLib;");
			printWriter.println();

			printWriter.println("@SuppressWarnings(\"unused\")");
			printWriter.println("public final class " + fullName + genericsFull.trim() + " extends JavaScriptObject {");
			printWriter.println("    protected " + fullName + "() {");
			printWriter.println("    }");
			printWriter.println();

			printWriter.println("    public static " + genericsFull + fullName + genericsShort + " create() {");
			if (type == Type.LIST) {
				printWriter.println("        return JavaScriptObject.createArray().cast();");
			} else {
				printWriter.println("        return JavaScriptObject.createObject().cast();");
			}
			printWriter.println("    }");
			printWriter.println();

			printWriter.println("    public native boolean contains(" + key.getSimpleName() + " idx) /*-{");
			printWriter.println("        return this.hasOwnProperty(" + keyString + ");");
			printWriter.println("    }-*/;");
			printWriter.println();

			if (type == Type.LIST) {
				printWriter.println("    public native int size() /*-{");
				printWriter.println("        return this.length;");
				printWriter.println("    }-*/;");
				printWriter.println();
				printWriter.println("    public native boolean isEmpty() /*-{");
				printWriter.println("        return !this.length;");
				printWriter.println("    }-*/;");
				printWriter.println();
				printWriter.println("    public native void remove(" + key.getSimpleName() + " idx) /*-{");
				printWriter.println("        this.splice(idx, 1);");
				printWriter.println("    }-*/;");
				printWriter.println();
				// TODO: long
				if (value != long.class) {
					printWriter.println("    public boolean forEach(" + getProcedureName(value, true) + " procedure) {");
					printWriter.println("        for (int i = 0; i < size(); i++) { ");
					printWriter.println("            if (!procedure.execute(get(i))) return false;");
					printWriter.println("        }");
					printWriter.println("        return true;");
					printWriter.println("    };");
					printWriter.println();
					printWriter.println("    public boolean forEach(" + getProcedureName(int.class, value, true) + " procedure) {");
					printWriter.println("        for (int i = 0; i < size(); i++) { ");
					printWriter.println("            if (!procedure.execute(i, get(i))) return false;");
					printWriter.println("        }");
					printWriter.println("        return true;");
					printWriter.println("    };");
					printWriter.println();
				}
			} else {
				if (type == Type.MAP) {
					if (value == Object.class || value == String.class) {
						printWriter.println("    public Iterable<" + valueType + "> keysIterable() {");
						printWriter.println("        return RpcUtils.<" + valueType + ">getMapIterable(this);");
						printWriter.println("    }");
						printWriter.println();
					}

					if (value != long.class) {
						printWriter.println("    public native boolean forEachEntry(" + getProcedureName(key, value, true)
								+ " procedure) /*-{");
						printWriter.println("        for (x in this) { ");
						printWriter.println("            if (this.hasOwnProperty(x)) {");
						printWriter.println("                if (!procedure.@" + packageName + "." + getProcedureName(key, value, false)
								+ "::execute(" + keyBinaryName + valueBinaryName + ")(x.slice(1), this[x])) return false;");
						printWriter.println("            }");
						printWriter.println("        }");
						printWriter.println("        return true;");
						printWriter.println("    }-*/;");
						printWriter.println();

						printWriter.println("    public native boolean forEachKey(" + getProcedureName(key, true) + " procedure) /*-{");
						printWriter.println("        for (x in this) { ");
						printWriter.println("            if (this.hasOwnProperty(x)) {");
						printWriter.println("                if (!procedure.@" + packageName + "." + getProcedureName(key, false)
								+ "::execute(" + keyBinaryName + ")(x.slice(1))) return false;");
						printWriter.println("            }");
						printWriter.println("        }");
						printWriter.println("        return true;");
						printWriter.println("    }-*/;");
						printWriter.println();

						printWriter.println("    public native boolean forEachValue(" + getProcedureName(value, true) + " procedure) /*-{");
						printWriter.println("        for (x in this) { ");
						printWriter.println("            if (this.hasOwnProperty(x)) {");
						printWriter.println("                if (!procedure.@" + packageName + "." + getProcedureName(value, false)
								+ "::execute(" + valueBinaryName + ")(this[x])) return false;");
						printWriter.println("            }");
						printWriter.println("        }");
						printWriter.println("        return true;");
						printWriter.println("    }-*/;");
						printWriter.println();
					}
				}
				if (type == Type.SET) {
					if (key == Object.class || key == String.class) {
						printWriter.println("    public Iterable<" + keyType + "> iterable() {");
						printWriter.println("        return RpcUtils.<" + keyType + ">getSetIterable(this);");
						printWriter.println("    }");
						printWriter.println();
					}
					printWriter.println("    public native boolean forEach(" + getProcedureName(key, true) + " procedure) /*-{");
					printWriter.println("        for (x in this) { ");
					printWriter.println("            if (this.hasOwnProperty(x)) {");
					printWriter.println("                if (!procedure.@" + packageName + "." + getProcedureName(key, false)
							+ "::execute(" + keyBinaryName + ")(x.slice(1))) return false;");
					printWriter.println("            }");
					printWriter.println("        }");
					printWriter.println("        return true;");
					printWriter.println("    }-*/;");
					printWriter.println();
				}
				printWriter.println("    /**");
				printWriter.println("     * Counts the size of a collection through brute force (slow).");
				printWriter.println("     */");
				printWriter.println("    public native int countSize() /*-{");
				printWriter.println("        var l = 0; for (x in this) if (this.hasOwnProperty(x)) l++; return l;");
				printWriter.println("    }-*/;");
				printWriter.println();
				printWriter.println("    public native boolean isEmpty() /*-{");
				printWriter.println("        for (x in this) if (this.hasOwnProperty(x)) return false; return true;");
				printWriter.println("    }-*/;");
				printWriter.println();
				printWriter.println("    public native void remove(" + key.getSimpleName() + " idx) /*-{");
				printWriter.println("        delete this" + indexer + ";");
				printWriter.println("    }-*/;");
				printWriter.println();
			}

			if (getter) {
				printWriter.println("    @UnsafeNativeLong");
				printWriter.println("    public native " + valueType + " get(" + key.getSimpleName() + " idx) /*-{");
				printWriter.println("        return " + String.format(safeGetter, "this" + indexer) + ";");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    @UnsafeNativeLong");
				printWriter.println("    public native void set(" + key.getSimpleName() + " idx, " + valueType + " value) /*-{");
				printWriter.println("        this" + indexer + " = " + safeSetter + ";");
				printWriter.println("    }-*/;");
				printWriter.println();
			}

			if (type == Type.LIST) {
				printWriter.println("    /**");
				printWriter.println("     * Adds an item to the end of the list, returning the list's new size.");
				printWriter.println("     */");
				printWriter.println("    @UnsafeNativeLong");
				printWriter.println("    public native int add(" + valueType + " value) /*-{");
				printWriter.println("        return this.push(" + safeSetter + ");");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    /**");
				printWriter.println("     * Adds an item to the end of the list, returning the list's new size.");
				printWriter.println("     */");
				printWriter.println("    @UnsafeNativeLong");
				printWriter.println("    public native int push(" + valueType + " value) /*-{");
				printWriter.println("        return this.push(" + safeSetter + ");");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    /**");
				printWriter.println("     * Pops an item off the end of the list, returning it.");
				printWriter.println("     */");
				printWriter.println("    @UnsafeNativeLong");
				printWriter.println("    public native " + valueType + " pop() /*-{");
				printWriter.println("        return " + String.format(safeGetter, "this.pop()") + ";");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    /**");
				printWriter.println("     * Peeks at the item at the end of the list.");
				printWriter.println("     */");
				printWriter.println("    public " + valueType + " peek() {");
				printWriter.println("        return this.get(this.size() - 1);");
				printWriter.println("    };");
				printWriter.println();

				printWriter.println("    /**");
				printWriter.println("     * Unshifts an item into position 0, returning the new size of the list.");
				printWriter.println("     */");
				printWriter.println("    @UnsafeNativeLong");
				printWriter.println("    public native int unshift(" + valueType + " value) /*-{");
				printWriter.println("        return this.unshift(" + safeSetter + ");");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    /**");
				printWriter.println("     * Shifts an item out of position 0 and returns it.");
				printWriter.println("     */");
				printWriter.println("    @UnsafeNativeLong");
				printWriter.println("    public native " + valueType + " shift() /*-{");
				printWriter.println("        return " + String.format(safeGetter, "this.shift()") + ";");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    public native String join(String separator) /*-{");
				printWriter.println("        return this.join(separator);");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    public native " + fullName + genericsShort + " slice(int index) /*-{");
				printWriter.println("        return this.slice(index);");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    public native " + fullName + genericsShort + " slice(int from, int to) /*-{");
				printWriter.println("        return this.slice(from, to);");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    public native " + fullName + genericsShort + " clear() /*-{");
				printWriter.println("        this.splice(0, this.length);");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    public native " + fullName + genericsShort + " splice(int index) /*-{");
				printWriter.println("        return this.splice(index, this.length);");
				printWriter.println("    }-*/;");
				printWriter.println();

				printWriter.println("    public native " + fullName + genericsShort + " splice(int index, int howMany) /*-{");
				printWriter.println("        return this.splice(index, howMany);");
				printWriter.println("    }-*/;");
				printWriter.println();
			}
			if (type == Type.SET) {
				printWriter.println("    public native void add(" + key.getSimpleName() + " idx) /*-{");
				printWriter.println("        this" + indexer + " = 0;");
				printWriter.println("    }-*/;");
				printWriter.println();
			}

			printWriter.println("}");
		} finally {
			printWriter.close();
		}
	}

	public static void main(String[] args) throws FileNotFoundException {
		new CollectionGen(new File("/Users/matthew/Documents/workspace/gwt-rpc-plus/gwt/com/dotspots/rpcplus/client/jscollections"),
				"com.dotspots.rpcplus.client.jscollections").generateCode();
	}
}
